-
Notifications
You must be signed in to change notification settings - Fork 5.9k
BIP Draft: FROST Signing Protocol for BIP340 Signatures #2070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
I'll fix the typos check soon |
|
I can see that GitHub's file changes view shows only one file at a time due to the large number of changes. This is because the reference implementation includes dependencies and auxiliary materials:
|
murchandamus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a first glance, but I noticed a few issues:
murchandamus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick turn-around. It’s on my todo list to give this a more thorough look, but it might take a bit. If you can motivate some other reviewers meanwhile, that would also be welcome.
I've shared it with most of the Bitcoin cryptographers I know and will post it on Twitter and the Bitcoin dev groups I'm part of. Hopefully that will bring in more reviewers! |
DarkWindman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! Quite a remarkable job! We found a few minor issues, and correcting them would improve the overall specification of the BIP.
Christewart
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned on X i'm working on this, so you will likely see more comments in the future. Another nice-to-have would be a table of contents (example) as most other BIPs have this. Perhaps this is a limitation of the .md document vs .mediawiki. Not sure.
| - Let *pubshare* = *empty_bytestring* | ||
| - If the optional argument *thresh_pk* is not present: | ||
| - Let *thresh_pk* = *empty_bytestring* | ||
| - If the optional argument *m* is not present: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you suggest handling this case in C?
We have a message m that equal to the empty bytestring? This is tested in the test vectors accompying this file here.
The python implmentation allows us to represent 2 different states that are (perhaps?) semantically mean the same thing is my understanding. Here are the 2 cases
mis not present (encoded as00) with the prefixmis present, but its the empty bytestring (encoded as0100) with the prefix.
This can be represented in the type system of higher level languages like C++, Python, Rust, Scala etc.
From looking at the API on zkp, it seems like this wouldn't be possible to represent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In C this must be modeled explicitly as an optional bytes type, otherwise we cannot distinguish
“m not present” (00) from “m present but empty” (0100).
I recommend representing this as { uint8_t *ptr; size_t len; bool is_present; } and encoding based on is_present.
Collapsing these cases would break the test vectors and change the hash domain.
#sc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We distinguish between a zeroed byte array and an empty byte string. Thus, m being absent (empty_bytestring) is different from m being equal to zero bytes (of any length), and we want to generate distinct nonces for these cases.
Yes, the current the zkp API doesn't add the message prefix correctly which needs to be fixed.
I agree with @vitrixLab, we can model this in C with an is_present variable, Musig2 does exactly this.
unsigned char msg_present;
msg_present = msg32 != NULL;
secp256k1_sha256_write(&sha, &msg_present, 1);
if (msg_present) {
secp256k1_nonce_function_musig_helper(&sha, 8, msg32, 32);
}
Yes, it's a |
Click there. ;)
|
Thank you! TIL :-) |
DarkWindman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few additional minor issues and questions.
| - For *i = 1 .. u*: | ||
| - Fail if not *0 ≤ id<sub>i</sub> ≤ n - 1* | ||
| - Fail if *cpoint(pubshare<sub>i</sub>)* fails | ||
| - Fail if *has_duplicates(id<sub>1..u</sub>)* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same function is also invoked in the DeriveInterpolatingValue() algorithm, which is called through the following chain:
ValidateSignersCtx()->DeriveThreshPubkey()->DeriveInterpolatingValue().
Is there a compelling reason to perform this check at the top level, or could the ID duplication check be retained solely at a lower level within DeriveInterpolatingValue()?
In addition, the ValidateSignersCtx() algorithm checks the condition t > n. However, as stated in the description, the valid range is 1 <= t <= n. Therefore, it would be clearer to replace the condition “Fail if t > n” with “Fail if not 1 <= t <= n”.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidateSignersCtx() checks the sanity of u signing participants (represented by ids and pubshares). So it only made sense to fail for all faults, like ids being out of range or containing duplicates. So, it's better to have it in the higher level?
I added has_duplicates again inside DeriveInterpolatingValue() because it has many other callers, just to be safe. Now that I look at it closely, all these calls get the ids & pubshares list after running ValidateSignersCtx() (through GetSessionValues), so we could technically remove the has_duplicates check from DeriveInterpolatingValue().
Therefore, it would be clearer to replace the condition “Fail if t > n” with “Fail if not 1 <= t <= n”.
I agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all these calls get the
ids&pubshareslist after runningValidateSignersCtx()(throughGetSessionValues), so we could technically remove thehas_duplicatescheck fromDeriveInterpolatingValue().
Yes, I agree with you, since we call GetSessionValues() at the beginning of both functions that independently invoke DeriveInterpolatingValue(). Thus, we can remove the check from the DeriveInterpolatingValue() and keep it only in ValidateSignersCtx().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright. I'll update this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to keep this redundant check in DeriveInterpolatingValue() for now. Just looked at RFC 9591 which defines their derive_interpolating_value function as:
def derive_interpolating_value(L, x_i):
if x_i not in L:
raise "invalid parameters"
for x_j in L:
if count(x_j, L) > 1:
raise "invalid parameters"
...
...
We can remove this has_duplicates check later, if we really need to.
| - If the optional argument *extra_in* is not present: | ||
| - Let *extra_in = empty_bytestring* | ||
| - Let *k<sub>i</sub> = scalar_from_bytes_wrapping(hash<sub>FROST/nonce</sub>(rand || bytes(1, len(pubshare)) || pubshare || bytes(1, len(thresh_pk)) || thresh_pk || m_prefixed || bytes(4, len(extra_in)) || extra_in || bytes(1, i - 1)))* for *i = 1,2* | ||
| - Fail if *k<sub>1</sub> = Scalar(0)* or *k<sub>2</sub> = Scalar(0)* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While reading the implementation, I noticed that it includes a check ensuring that k_1 != k_2. At first glance, omitting this check does not appear to introduce any vulnerabilities, and we have verified this. However, I would appreciate hearing your opinion on this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which function are you referring to? I don't see a k_1 != k_2 check in the nonce_gen_internal or deterministic_sign functions.
This is an interesting question though. I never considered adding this check, primarily because BIP327's reference implementation doesn't have it either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which function are you referring to? I don't see a
k_1 != k_2check in thenonce_gen_internalordeterministic_signfunctions.
I apologize for not mentioning which implementation I was referring to. I meant the secp256k1-zkp FROST module, where the secp256k1_frost_nonce_gen() function performs this check. However, I think this verification is redundant, as I have not found any paper or specification that requires it. At first glance, it may seem that manipulation with Wagner attacks could apply, but I do not see any concrete attack vectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I was not aware of this. Thank you! I checked the Olaf paper as well, and it didn't have this requirement. I haven't thought about this from security proof perspective. Will keep this open till then :)
|
@DarkWindman thanks a lot for the review! I've addressed most of your review comments in a88f033. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From an editorial standpoint, it looks pretty good and like all the required sections are present. I have read the proposal only partially, and do not have the expertise to fully understand all aspects, so I cannot comment on the technical soundness and whether the Specification is complete and sufficient.
|
|
||
| ## Motivation | ||
|
|
||
| <!-- REVIEW: Should we add a paragraph about `OP_CHECKSIGADD` like BIP327 does? --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Should we add a paragraph about
OP_CHECKSIGADDlike BIP327 does?"
Sounds reasonable to add that. If I'm not missing anything, the two paragraphs in BIP327 would fully apply to FROST as well (after minor adaptions, s/MuSig2/FROST/ and s/n-of-n/t-of-n/).
| This treatment may be confusing for readers familiar with the MuSig2 paper. | ||
| However, serialization is a technical detail that is irrelevant for users of MuSig2 interfaces. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should "MuSig2" replaced with "FROST" here?
| | Notation | secp256k1lab | Description | | ||
| | --- | --- | --- | | ||
| | *p* | *FE.SIZE* | Field element size | | ||
| | *ord* | *GE.ORDER* | Group order | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the constant is also available as Scalar.SIZE in secp256k1lab
| | *ord* | *GE.ORDER* | Group order | | ||
| | *G* | *G* | The secp256k1 generator point | | ||
| | *inf_point* | *GE()* | The infinity point | | ||
| | *is_infinity(P)* | *P.infinity()* | Returns whether *P* is the point at infinity | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| | *is_infinity(P)* | *P.infinity()* | Returns whether *P* is the point at infinity | | |
| | *is_infinity(P)* | *P.infinity* | Returns whether *P* is the point at infinity | |
(since infinity is a property rather than a method within GE)
| - The accumulated tweak *tacc*: a *Scalar* | ||
| - The value *gacc*: *Scalar(1)* or *Scalar(-1)* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pedantic nit: could swap these two lines, so the order matches the one of unpacking tweak_ctx (and the order of TweakCtxInit return values) below
| - Fail if *k<sub>1</sub> = Scalar(0)* or *k<sub>2</sub> = Scalar(0)* | ||
| - Let *R<sub>\*,1</sub> = k<sub>1</sub> · G*, *R<sub>\*,2</sub> = k<sub>2</sub> · G* | ||
| - Let *pubnonce = cbytes(R<sub>\*,1</sub>) || cbytes(R<sub>\*,2</sub>)* | ||
| - Let *secnonce = bytes(32, k<sub>1</sub>) || bytes(32, k<sub>2</sub>)*[^secnonce-ser] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could use scalar_to_bytes here instead
| - Inputs: | ||
| - The number *u* of signing participants: an integer with *t ≤ u ≤ n* | ||
| - The list of participant public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte array, each an output of *NonceGen* | ||
| - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* < *n* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pedantic nit, for consistency with other functions:
| - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* < *n* | |
| - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* <= *n-1* |
|
|
||
| - Inputs: | ||
| - The partial signature *psig*: a 32-byte array, serialized scalar | ||
| - The list public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte arrays, each an output of *NonceGen* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - The list public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte arrays, each an output of *NonceGen* | |
| - The list of public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte arrays, each an output of *NonceGen* |
| ], | ||
| "msg_index": 0, | ||
| "signer_index": 0, | ||
| "comment": "The signer's pubshare is not in the list of pubshares" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this test should be moved into sign_error_test_cases since this would cause an error when deriving the threshold public key from the pubshares?
Fail if DeriveThreshPubkey(id1..u, pubshare1..u) ≠ thresh_pk

This PR adds a BIP for the FROST (Flexible Round-Optimized Schnorr Threshold) signing protocol. The development repository is at https://github.com/siv2r/bip-frost-signing.
There already exists RFC 9591, which standardizes the two-round FROST signing protocol, but it is incompatible with Bitcoin's BIP340 X-only public keys. This BIP bridges that gap by providing a BIP340-compatible variant of FROST.
This BIP standardizes the FROST3 variant (Section 2.3 of the ROAST paper). This variant shares significant similarities with the MuSig2 signing protocol (BIP327). Accordingly, this BIP follows the core design principles of BIP327, and many sections have been directly adapted from it.
FROST key generation is out of scope for this BIP. There are sister BIPs such as ChillDKG and Trusted Dealer Generation that specify key generation mechanisms. This BIP must be used in conjunction with either of those for the full workflow from key generation to signature creation. Careful consideration has been taken to ensure the terminology in this BIP matches that of ChillDKG.
There are multiple (experimental) implementations of this specification:
FROST-BIP340TODO: verify if this impl is compatible with our test vectorssecp256kfun (implements ChillDKG with FROST signing)TODO: verify if this impl is compatible with our test vectorsDisclosure: AI has been used to rephrase paragraphs for clarity, refactor certain sections of the reference code, and review pull requests made to the development repository.
Feedback is appreciated! Please comment on this pull request or open an issue at https://github.com/siv2r/bip-frost-signing for any feedback. Thank you!
cc @jonasnick @real-or-random @jesseposner